package com.googlecode.eventio.reflection;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import com.googlecode.eventio.EventExecutor;
import com.googlecode.eventio.Reaction;
import com.googlecode.eventio.Result;

public class AsyncProxyFactory {

	private final Map<ClassPair, Preprocessed> preprocessedRegistry = new HashMap<ClassPair, Preprocessed>();

	private final EventExecutor eventExecutor;

	public AsyncProxyFactory(EventExecutor eventExecutor) {
		super();
		this.eventExecutor = eventExecutor;
	}

	private Preprocessed process(Class<?> proxy, Class<?> target) {
		Map<Method, Method> methodMapping = new HashMap<Method, Method>();

		//check well-formedness of proxy interface
		for (Method method : proxy.getMethods()) {

			Class<?>[] parameterTypes = new Class<?>[method.getParameterTypes().length - 1];
			System.arraycopy(method.getParameterTypes(), 0, parameterTypes, 0,
					parameterTypes.length);
			Method targetMethod;
			try {

				targetMethod = target.getMethod(method.getName(), parameterTypes);

			} catch (NoSuchMethodException e) {
				return new Preprocessed(new IllegalArgumentException("method " + method.getName()
						+ Arrays.toString(parameterTypes) + " not found in target class " + target));
			}

			List<Class<?>> targetMethodParameterTypes = Arrays.asList(targetMethod
					.getParameterTypes());
			Deque<Class<?>> proxyMethodParameterTypes = new LinkedList<Class<?>>(
					Arrays.asList(method.getParameterTypes()));

			proxyMethodParameterTypes.removeLast();

			if (!targetMethodParameterTypes.equals(proxyMethodParameterTypes)) {
				throw new IllegalArgumentException("method argument mismatch. Expected "
						+ targetMethodParameterTypes + ", found " + proxyMethodParameterTypes);

			}

			Class<?> targetReturnType = targetMethod.getReturnType();

			boolean found = false;
			Type reactionType = method.getGenericParameterTypes()[proxyMethodParameterTypes.size()];
			if (reactionType instanceof ParameterizedType) {
				ParameterizedType paramReactionType = (ParameterizedType) reactionType;
				if (paramReactionType.getRawType().equals(Reaction.class)) {
					Type[] resultTypes = paramReactionType.getActualTypeArguments();
					if (resultTypes.length == 1) {
						if (resultTypes[0] instanceof ParameterizedType) {
							ParameterizedType paramResultType = (ParameterizedType) resultTypes[0];
							if (paramResultType.getRawType().equals(Result.class)) {
								Type[] outputTypes = paramResultType.getActualTypeArguments();
								if (outputTypes.length == 1) {
									if (outputTypes[0].equals(targetReturnType)) {
										found = true;
									} else if (outputTypes[0].equals(Void.class)
											&& targetReturnType.equals(void.class)) {
										found = true;
									}

								}
							}
						}
					}
				}
			}

			if (!found) {
				return new Preprocessed(new IllegalArgumentException(
						"last argument in async proxy method " + method.getName()
								+ " must be a Reaction<Result<" + targetReturnType.getSimpleName()
								+ ">>"));
			}

			methodMapping.put(method, targetMethod);
		}

		return new Preprocessed(methodMapping);
	}

	@SuppressWarnings("unchecked")
	public <T> T newAsyncProxy(final Class<T> proxyInterface, final Object target) {

		return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
				new Class<?>[] { proxyInterface }, new InvocationHandler() {

					//cache mapping upon handler construction
					final Map<Method, Method> methodMapping;
					{
						ClassPair key = new ClassPair(proxyInterface, target.getClass());
						Preprocessed cached = preprocessedRegistry.get(key);
						if (cached == null) {
							cached = process(key.c1, key.c2);
							preprocessedRegistry.put(key, cached);
						}
						methodMapping = cached.getMethodMapping();

					}

					@Override
					public Object invoke(Object proxy, Method method, Object[] args)
							throws Throwable {

						final Method targetMethod = methodMapping.get(method);

						final Object[] targetArgs = new Object[args.length - 1];
						System.arraycopy(args, 0, targetArgs, 0, targetArgs.length);

						eventExecutor.execute(new Callable<Object>() {
							@Override
							public Object call() throws Exception {
								return targetMethod.invoke(target, targetArgs);
							}
						}, (Reaction<Result<Object>>) args[args.length - 1]);

						return null;
					}
				});

	}

	private static class ClassPair {
		private final Class<?> c1;
		private final Class<?> c2;

		public ClassPair(Class<?> c1, Class<?> c2) {
			super();
			this.c1 = c1;
			this.c2 = c2;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = (prime * result) + ((c1 == null) ? 0 : c1.hashCode());
			result = (prime * result) + ((c2 == null) ? 0 : c2.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			if (obj == null) {
				return false;
			}
			if (getClass() != obj.getClass()) {
				return false;
			}
			ClassPair other = (ClassPair) obj;
			if (c1 == null) {
				if (other.c1 != null) {
					return false;
				}
			} else if (!c1.equals(other.c1)) {
				return false;
			}
			if (c2 == null) {
				if (other.c2 != null) {
					return false;
				}
			} else if (!c2.equals(other.c2)) {
				return false;
			}
			return true;
		}

	}

	private static class Preprocessed {
		private final Map<Method, Method> methodMapping;
		private final RuntimeException creationException;

		public Preprocessed(RuntimeException creationException) {
			super();
			this.methodMapping = null;
			this.creationException = creationException;
		}

		public Preprocessed(Map<Method, Method> methodMapping) {
			super();
			this.methodMapping = new HashMap<Method, Method>(methodMapping);
			this.creationException = null;
		}

		public Map<Method, Method> getMethodMapping() throws RuntimeException {
			if (creationException != null) {
				throw creationException;
			}
			return methodMapping;
		}

	}

}
